home *** CD-ROM | disk | FTP | other *** search
/ Enter 2006 September / Enter 09 2006.iso / Internet / SpamExperts Home 1.1 / SpamExperts Home.exe / lib / spamexperts.modules / spambayes / smtpproxy.pyc (.txt) < prev    next >
Encoding:
Python Compiled Bytecode  |  2006-07-14  |  19.4 KB  |  482 lines

  1. # Source Generated with Decompyle++
  2. # File: in.pyc (Python 2.4)
  3.  
  4. '''A SMTP proxy to train a Spambayes database.
  5.  
  6. You point SMTP Proxy at your SMTP server(s) and configure your email
  7. client(s) to send mail through the proxy (i.e. usually this means you use
  8. localhost as the outgoing server).
  9.  
  10. To setup, enter appropriate values in your Spambayes configuration file in
  11. the "SMTP Proxy" section (in particular: "remote_servers", "listen_ports",
  12. and "use_cached_message").  This configuration can also be carried out via
  13. the web user interface offered by POP3 Proxy and IMAP Filter.
  14.  
  15. To use, simply forward/bounce mail that you wish to train to the
  16. appropriate address (defaults to spambayes_spam@localhost and
  17. spambayes_ham@localhost).  All other mail is sent normally.
  18. (Note that IMAP Filter and POP3 Proxy users should not execute this script;
  19. launching of SMTP Proxy will be taken care of by those applicatons).
  20.  
  21. There are two main forms of operation.  With both, mail to two
  22. (user-configurable) email addresses is intercepted by the proxy (and is
  23. *not* sent to the SMTP server) and used as training data for a Spambayes
  24. database.  All other mail is simply relayed to the SMTP server.
  25.  
  26. If the "use_cached_message" option is False, the proxy uses the message
  27. sent as training data.  This option is suitable for those not using
  28. POP3 Proxy or IMAP Filter, or for those that are confident that their
  29. mailer will forward/bounce messages in an unaltered form.
  30.  
  31. If the "use_cached_message" option is True, the proxy examines the message
  32. for a unique spambayes identification number.  It then tries to find this
  33. message in the pop3proxy caches and on the imap servers.  It then retrieves
  34. the message from the cache/server and uses *this* as the training data.
  35. This method is suitable for those using POP3 Proxy and/or IMAP Filter, and
  36. avoids any potential problems with the mailer altering messages before
  37. forwarding/bouncing them.
  38.  
  39. To use, enter the required SMTP server data in your configuration file and
  40. run sb_server.py
  41. '''
  42. __author__ = 'Tony Meyer <ta-meyer@ihug.co.nz>'
  43. __credits__ = 'Tim Stone, all the Spambayes folk.'
  44.  
  45. try:
  46.     (True, False)
  47. except NameError:
  48.     (True, False) = (1, 0)
  49.  
  50. todo = "\n o It would be nice if spam/ham could be bulk forwarded to the proxy,\n   rather than one by one.  This would require separating the different\n   messages and extracting the correct ids.  Simply changing to find\n   *all* the ids in a message, rather than stopping after one *might*\n   work, but I don't really know.  Richie Hindle suggested something along\n   these lines back in September '02.\n\n o Suggestions?\n\nTesting:\n\n o Test with as many clients as possible to check that the\n   id is correctly extracted from the forwarded/bounced message.\n\nMUA information:\nA '*' in the Header column signifies that the smtpproxy can extract\nthe id from the headers only.  A '*' in the Body column signifies that\nthe smtpproxy can extract the id from the body of the message, if it\nis there.\n                                                        Header  Body\n*** Windows 2000 MUAs ***\nEudora 5.2 Forward                                         *     *\nEudora 5.2 Redirect                                              *\nNetscape Messenger (4.7) Forward (inline)                  *     *\nNetscape Messenger (4.7) Forward (quoted) Plain                  *\nNetscape Messenger (4.7) Forward (quoted) HTML                   *\nNetscape Messenger (4.7) Forward (quoted) Plain & HTML           *\nNetscape Messenger (4.7) Forward (attachment) Plain        *     *\nNetscape Messenger (4.7) Forward (attachment) HTML         *     *\nNetscape Messenger (4.7) Forward (attachment) Plain & HTML *     *\nOutlook Express 6 Forward HTML (Base64)                          *\nOutlook Express 6 Forward HTML (None)                            *\nOutlook Express 6 Forward HTML (QP)                              *\nOutlook Express 6 Forward Plain (Base64)                         *\nOutlook Express 6 Forward Plain (None)                           *\nOutlook Express 6 Forward Plain (QP)                             *\nOutlook Express 6 Forward Plain (uuencoded)                      *\nhttp://www.endymion.com/products/mailman Forward                     *\nM2 (Opera Mailer 7.01) Forward                                   *\nM2 (Opera Mailer 7.01) Redirect                            *     *\nThe Bat! 1.62i Forward (RFC Headers not visible)                 *\nThe Bat! 1.62i Forward (RFC Headers visible)               *     *\nThe Bat! 1.62i Redirect                                          *\nThe Bat! 1.62i Alternative Forward                         *     *\nThe Bat! 1.62i Custom Template                             *     *\nAllegroMail 2.5.0.2 Forward                                      *\nAllegroMail 2.5.0.2 Redirect                                     *\nPocoMail 2.6.3 Bounce                                            *\nPocoMail 2.6.3 Bounce                                            *\nPegasus Mail 4.02 Forward (all headers option set)         *     *\nPegasus Mail 4.02 Forward (all headers option not set)           *\nCalypso 3 Forward                                                *\nCalypso 3 Redirect                                         *     *\nBecky! 2.05.10 Forward                                           *\nBecky! 2.05.10 Redirect                                          *\nBecky! 2.05.10 Redirect as attachment                      *     *\nMozilla Mail 1.2.1 Forward (attachment)                    *     *\nMozilla Mail 1.2.1 Forward (inline, plain)                 *1    *\nMozilla Mail 1.2.1 Forward (inline, plain & html)          *1    *\nMozilla Mail 1.2.1 Forward (inline, html)                  *1    *\n\n*1 The header method will only work if auto-include original message\nis set, and if view all headers is true.\n"
  51. import string
  52. import re
  53. import socket
  54. import asyncore
  55. import asynchat
  56. import getopt
  57. import sys
  58. import os
  59. import email
  60. from spambayes import Dibbler
  61. from spambayes import storage
  62. from spambayes import message
  63. from spambayes.tokenizer import textparts
  64. from spambayes.tokenizer import try_to_repair_damaged_base64
  65. from spambayes.Options import options
  66. from sb_server import _addressPortStr, ServerLineReader
  67. from sb_server import _addressAndPort
  68.  
  69. class SMTPProxyBase(Dibbler.BrighterAsyncChat):
  70.     """An async dispatcher that understands SMTP and proxies to a SMTP
  71.     server, calling `self.onTransaction(command, args)` for each
  72.     transaction.
  73.  
  74.     self.onTransaction() should return the command to pass to
  75.     the proxied server - the command can be the verbatim command or a
  76.     processed version of it.  The special command 'KILL' kills it (passing
  77.     a 'QUIT' command to the server).
  78.     """
  79.     
  80.     def __init__(self, clientSocket, serverName, serverPort):
  81.         Dibbler.BrighterAsyncChat.__init__(self, clientSocket)
  82.         self.request = ''
  83.         self.set_terminator('\r\n')
  84.         self.command = ''
  85.         self.args = ''
  86.         self.isClosing = False
  87.         self.inData = False
  88.         self.data = []
  89.         self.blockData = False
  90.         if not self.onIncomingConnection(clientSocket):
  91.             self.push('421 Connection not allowed\r\n')
  92.             self.close_when_done()
  93.             return None
  94.         
  95.         self.serverSocket = ServerLineReader(serverName, serverPort, self.onServerLine)
  96.  
  97.     
  98.     def onIncomingConnection(self, clientSocket):
  99.         '''Checks the security settings.'''
  100.         remoteIP = clientSocket.getpeername()[0]
  101.         trustedIPs = options[('smtpproxy', 'allow_remote_connections')]
  102.         if trustedIPs == '*' or remoteIP == clientSocket.getsockname()[0]:
  103.             return True
  104.         
  105.         trustedIPs = trustedIPs.replace('.', '\\.').replace('*', '([01]?\\d\\d?|2[04]\\d|25[0-5])')
  106.         for trusted in trustedIPs.split(','):
  107.             if re.search('^' + trusted + '$', remoteIP):
  108.                 return True
  109.                 continue
  110.         
  111.         return False
  112.  
  113.     
  114.     def onTransaction(self, command, args):
  115.         '''Overide this.  Takes the raw command and returns the (possibly
  116.         processed) command to pass to the email client.'''
  117.         raise NotImplementedError
  118.  
  119.     
  120.     def onProcessData(self, data):
  121.         '''Overide this.  Takes the raw data and returns the (possibly
  122.         processed) data to pass back to the email client.'''
  123.         raise NotImplementedError
  124.  
  125.     
  126.     def onServerLine(self, line):
  127.         '''A line of response has been received from the SMTP server.'''
  128.         if not line:
  129.             self.isClosing = True
  130.         
  131.         self.push(line)
  132.         self.onResponse()
  133.  
  134.     
  135.     def collect_incoming_data(self, data):
  136.         '''Asynchat override.'''
  137.         self.request = self.request + data
  138.  
  139.     
  140.     def found_terminator(self):
  141.         '''Asynchat override.'''
  142.         verb = self.request.strip().upper()
  143.         if verb == 'KILL':
  144.             self.socket.shutdown(2)
  145.             self.close()
  146.             raise SystemExit
  147.         
  148.         if self.request.strip() == '':
  149.             self.command = self.args = ''
  150.         elif self.request[:10].upper() == 'MAIL FROM:':
  151.             splitCommand = self.request.split(':', 1)
  152.         elif self.request[:8].upper() == 'RCPT TO:':
  153.             splitCommand = self.request.split(':', 1)
  154.         else:
  155.             splitCommand = self.request.strip().split(None, 1)
  156.         self.command = splitCommand[0]
  157.         self.args = splitCommand[1:]
  158.         if self.inData == True:
  159.             self.data.append(self.request + '\r\n')
  160.             if self.request == '.':
  161.                 self.inData = False
  162.                 cooked = self.onProcessData(''.join(self.data))
  163.                 self.data = []
  164.                 if self.blockData == False:
  165.                     self.serverSocket.push(cooked)
  166.                 else:
  167.                     self.push('250 OK\r\n')
  168.             
  169.         else:
  170.             cooked = self.onTransaction(self.command, self.args)
  171.             if cooked is not None:
  172.                 self.serverSocket.push(cooked + '\r\n')
  173.             
  174.         self.command = self.args = self.request = ''
  175.  
  176.     
  177.     def onResponse(self):
  178.         if self.isClosing:
  179.             self.close_when_done()
  180.         
  181.         self.command = ''
  182.         self.args = ''
  183.         self.isClosing = False
  184.  
  185.  
  186.  
  187. class BayesSMTPProxyListener(Dibbler.Listener):
  188.     '''Listens for incoming email client connections and spins off
  189.     BayesSMTPProxy objects to serve them.'''
  190.     
  191.     def __init__(self, serverName, serverPort, proxyPort, trainer):
  192.         proxyArgs = (serverName, serverPort, trainer)
  193.         Dibbler.Listener.__init__(self, proxyPort, BayesSMTPProxy, proxyArgs)
  194.         print 'SMTP Listener on port %s is proxying %s:%d' % (_addressPortStr(proxyPort), serverName, serverPort)
  195.  
  196.  
  197.  
  198. class BayesSMTPProxy(SMTPProxyBase):
  199.     '''Proxies between an email client and a SMTP server, inserting
  200.     judgement headers.  It acts on the following SMTP commands:
  201.  
  202.     o RCPT TO:
  203.         o Checks if the recipient address matches the key ham or spam
  204.           addresses, and if so notes this and does not forward a command to
  205.           the proxied server.  In all other cases simply passes on the
  206.           verbatim command.
  207.  
  208.      o DATA:
  209.         o Notes that we are in the data section.  If (from the RCPT TO
  210.           information) we are receiving a ham/spam message to train on,
  211.           then do not forward the command on.  Otherwise forward verbatim.
  212.  
  213.     Any other commands are merely passed on verbatim to the server.
  214.     '''
  215.     
  216.     def __init__(self, clientSocket, serverName, serverPort, trainer):
  217.         SMTPProxyBase.__init__(self, clientSocket, serverName, serverPort)
  218.         self.handlers = {
  219.             'RCPT TO': self.onRcptTo,
  220.             'DATA': self.onData,
  221.             'MAIL FROM': self.onMailFrom }
  222.         self.trainer = trainer
  223.         self.isClosed = False
  224.         self.train_as_ham = False
  225.         self.train_as_spam = False
  226.  
  227.     
  228.     def send(self, data):
  229.         
  230.         try:
  231.             return SMTPProxyBase.send(self, data)
  232.         except socket.error:
  233.             self.close()
  234.  
  235.  
  236.     
  237.     def close(self):
  238.         if not self.isClosed:
  239.             self.isClosed = True
  240.             SMTPProxyBase.close(self)
  241.         
  242.  
  243.     
  244.     def stripAddress(self, address):
  245.         '''
  246.         Strip the leading & trailing <> from an address.  Handy for
  247.         getting FROM: addresses.
  248.         '''
  249.         if '<' in address:
  250.             start = string.index(address, '<') + 1
  251.             end = string.index(address, '>')
  252.             return address[start:end]
  253.         else:
  254.             return address
  255.  
  256.     
  257.     def onTransaction(self, command, args):
  258.         handler = self.handlers.get(command.upper(), self.onUnknown)
  259.         return handler(command, args)
  260.  
  261.     
  262.     def onProcessData(self, data):
  263.         if self.train_as_spam:
  264.             self.trainer.train(data, True)
  265.             self.train_as_spam = False
  266.             return ''
  267.         elif self.train_as_ham:
  268.             self.trainer.train(data, False)
  269.             self.train_as_ham = False
  270.             return ''
  271.         
  272.         return data
  273.  
  274.     
  275.     def onRcptTo(self, command, args):
  276.         toFull = self.stripAddress(args[0])
  277.         if toFull == options[('smtpproxy', 'spam_address')]:
  278.             self.train_as_spam = True
  279.             self.train_as_ham = False
  280.             self.blockData = True
  281.             self.push('250 OK\r\n')
  282.             return None
  283.         elif toFull == options[('smtpproxy', 'ham_address')]:
  284.             self.train_as_ham = True
  285.             self.train_as_spam = False
  286.             self.blockData = True
  287.             self.push('250 OK\r\n')
  288.             return None
  289.         else:
  290.             self.blockData = False
  291.         return '%s:%s' % (command, ' '.join(args))
  292.  
  293.     
  294.     def onData(self, command, args):
  295.         self.inData = True
  296.         if self.train_as_ham == True or self.train_as_spam == True:
  297.             self.push('354 Enter data ending with a . on a line by itself\r\n')
  298.             return None
  299.         
  300.         return command + ' ' + ' '.join(args)
  301.  
  302.     
  303.     def onMailFrom(self, command, args):
  304.         '''Just like the default handler, but has the necessary colon.'''
  305.         rv = '%s:%s' % (command, ' '.join(args))
  306.         return rv
  307.  
  308.     
  309.     def onUnknown(self, command, args):
  310.         '''Default handler.'''
  311.         return self.request
  312.  
  313.  
  314.  
  315. class SMTPTrainer(object):
  316.     
  317.     def __init__(self, classifier, state = None, imap = None):
  318.         self.classifier = classifier
  319.         self.state = state
  320.         self.imap = imap
  321.  
  322.     
  323.     def extractSpambayesID(self, data):
  324.         msg = email.message_from_string(data, _class = message.SBHeaderMessage)
  325.         id = msg.get(options[('Headers', 'mailid_header_name')])
  326.         if id is not None:
  327.             return id
  328.         
  329.         id = self._find_id_in_text(msg.as_string())
  330.         if id is not None:
  331.             return id
  332.         
  333.         for part in textparts(msg):
  334.             
  335.             try:
  336.                 text = part.get_payload(decode = True)
  337.             except:
  338.                 text = part.get_payload(decode = False)
  339.                 if text is not None:
  340.                     text = try_to_repair_damaged_base64(text)
  341.                 
  342.  
  343.             if text is not None:
  344.                 id = self._find_id_in_text(text)
  345.                 return id
  346.                 continue
  347.         
  348.  
  349.     header_pattern = re.escape(options[('Headers', 'mailid_header_name')])
  350.     header_pattern += ':\\s*(\\</th\\>\\s*\\<td\\>\\s*)?([\\d\\-]+)'
  351.     header_re = re.compile(header_pattern)
  352.     
  353.     def _find_id_in_text(self, text):
  354.         mo = self.header_re.search(text)
  355.         if mo is None:
  356.             return None
  357.         
  358.         return mo.group(2)
  359.  
  360.     
  361.     def train(self, msg, isSpam):
  362.         
  363.         try:
  364.             use_cached = options[('smtpproxy', 'use_cached_message')]
  365.         except KeyError:
  366.             use_cached = True
  367.  
  368.         if use_cached:
  369.             id = self.extractSpambayesID(msg)
  370.             if id is None:
  371.                 print 'Could not extract id'
  372.                 return None
  373.             
  374.             self.train_cached_message(id, isSpam)
  375.         
  376.         msg = email.message_from_string(msg, _class = message.SBHeaderMessage)
  377.         id = msg.setIdFromPayload()
  378.         msg.delSBHeaders()
  379.         if id is None:
  380.             self.classifier.learn(msg.tokenize(), isSpam)
  381.         elif msg.GetTrained() == (not isSpam):
  382.             self.classifier.unlearn(msg.tokenize(), not isSpam)
  383.             msg.RememberTrained(None)
  384.         
  385.         if msg.GetTrained() is None:
  386.             self.classifier.learn(msg.tokenize(), isSpam)
  387.             msg.RememberTrained(isSpam)
  388.         
  389.  
  390.     
  391.     def train_cached_message(self, id, isSpam):
  392.         if not self.train_message_in_pop3proxy_cache(id, isSpam) and not self.train_message_on_imap_server(id, isSpam):
  393.             print 'Could not find message (%s); perhaps it was deleted from the POP3Proxy cache or the IMAP server.  This means that no training was done.' % (id,)
  394.         
  395.  
  396.     
  397.     def train_message_in_pop3proxy_cache(self, id, isSpam):
  398.         if self.state is None:
  399.             return False
  400.         
  401.         sourceCorpus = None
  402.         for corpus in [
  403.             self.state.unknownCorpus,
  404.             self.state.hamCorpus,
  405.             self.state.spamCorpus]:
  406.             if corpus.get(id) is not None:
  407.                 sourceCorpus = corpus
  408.                 break
  409.                 continue
  410.         
  411.         if corpus is None:
  412.             return False
  413.         
  414.         if isSpam == True:
  415.             targetCorpus = self.state.spamCorpus
  416.         else:
  417.             targetCorpus = self.state.hamCorpus
  418.         targetCorpus.takeMessage(id, sourceCorpus)
  419.         self.classifier.store()
  420.         return True
  421.  
  422.     
  423.     def train_message_on_imap_server(self, id, isSpam):
  424.         if self.imap is None:
  425.             return False
  426.         
  427.         msg = self.imap.FindMessage(id)
  428.         if msg is None:
  429.             return False
  430.         
  431.         if msg.GetTrained() == (not isSpam):
  432.             msg.get_substance()
  433.             msg.delSBHeaders()
  434.             self.classifier.unlearn(msg.tokenize(), not isSpam)
  435.             msg.RememberTrained(None)
  436.         
  437.         if msg.GetTrained() is None:
  438.             msg.get_substance()
  439.             msg.delSBHeaders()
  440.             self.classifier.learn(msg.tokenize(), isSpam)
  441.             msg.RememberTrained(isSpam)
  442.         
  443.         self.classifier.store()
  444.         return True
  445.  
  446.  
  447.  
  448. def LoadServerInfo():
  449.     servers = []
  450.     proxyPorts = []
  451.     if options[('smtpproxy', 'remote_servers')]:
  452.         for server in options[('smtpproxy', 'remote_servers')]:
  453.             server = server.strip()
  454.             if server.find(':') > -1:
  455.                 (server, port) = server.split(':', 1)
  456.             else:
  457.                 port = '25'
  458.             servers.append((server, int(port)))
  459.         
  460.     
  461.     if options[('smtpproxy', 'listen_ports')]:
  462.         splitPorts = options[('smtpproxy', 'listen_ports')]
  463.         proxyPorts = map(_addressAndPort, splitPorts)
  464.     
  465.     if len(servers) != len(proxyPorts):
  466.         print 'smtpproxy:remote_servers & smtpproxy:listen_ports are ' + 'different lengths!'
  467.         sys.exit()
  468.     
  469.     return (servers, proxyPorts)
  470.  
  471.  
  472. def CreateProxies(servers, proxyPorts, trainer):
  473.     '''Create BayesSMTPProxyListeners for all the given servers.'''
  474.     proxyListeners = []
  475.     for server, serverPort in zip(servers, proxyPorts):
  476.         proxyPort = None
  477.         listener = BayesSMTPProxyListener(server, serverPort, proxyPort, trainer)
  478.         proxyListeners.append(listener)
  479.     
  480.     return proxyListeners
  481.  
  482.